using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web.UI;
using System.Text;
using System.Collections.Specialized;

using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Represents bindings between <see cref="IValueObject"/> fields and user interface controls. Each control is bound to a single field.
    /// Manages how the <see cref="IBusinessObject"/> and <see cref="ISelfValidatingObject"/> is bound to UI controls.
	/// </summary>
	[Serializable]
    public sealed class BusinessObjectBindingContext : IValueObjectBindingContext
	{
		#region FieldPermissions.

        List<IFieldBinding> bindings = new List<IFieldBinding>();
		ITextControl mainErrorLabel;
		IMessageFormatter formatter;

		// Parser error descriptions.
		string defaultParseError;
		string integerParseError;
		string numberParseError;
		string datetimeParseError;

		#endregion

		#region Constructors.

		/// <summary>
		/// Initializes a new instance of <b>BusinessObjectBindingContext</b> class.
		/// </summary>
		/// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the bound values displayed in the controlls.</param>
		public BusinessObjectBindingContext(IMessageFormatter formatter)
		{
			this.formatter = formatter;
		}

        /// <summary>
        /// Initializes a new instance of <b>BusinessObjectBindingContext</b> class.
        /// </summary>
        /// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the bound values displayed in the controlls.</param>
        /// <param name="mainErrorLabel">Main/default error label to which all unhandled errors are written.</param>
        public BusinessObjectBindingContext(IMessageFormatter formatter, ITextControl mainErrorLabel)
        {
            this.formatter = formatter;
            SetMainErrorLabel(mainErrorLabel);
        }

        /// <summary>
        /// Initializes a new instance of <b>BusinessObjectBindingContext</b> class.
        /// </summary>
        /// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the bound values displayed in the controlls.</param>
        /// <param name="mainErrorLabel">Main/default error label to which all unhandled errors are written.</param>
        /// <param name="defaultParseError">Generic message displayed when a parsing error occures.</param>
        public BusinessObjectBindingContext(IMessageFormatter formatter, ITextControl mainErrorLabel, string defaultParseError)
        {
            this.formatter = formatter;
            SetMainErrorLabel(mainErrorLabel);
            this.defaultParseError = defaultParseError;
        }

        /// <summary>
        /// Initializes a new instance of <b>BusinessObjectBindingContext</b> class.
        /// </summary>
        /// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the bound values displayed in the controlls.</param>
        /// <param name="mainErrorLabel">Main/default error label to which all unhandled errors are written.</param>
        /// <param name="defaultParseError">Message displayed when a parsing error occures.</param>
        /// <param name="integerParseError">Message displayed when parsing of integer value fails.</param>
        /// <param name="numberParseError">Message displayed when number parsing fails.</param>
        /// <param name="datetimeParseError">Message displayed when parsing of DateTime value fails.</param>
        public BusinessObjectBindingContext(IMessageFormatter formatter, ITextControl mainErrorLabel,
            string defaultParseError, string integerParseError, string numberParseError, string datetimeParseError)
        {
            this.formatter = formatter;
            SetMainErrorLabel(mainErrorLabel);
            this.defaultParseError = defaultParseError;
            this.integerParseError = integerParseError;
            this.numberParseError = numberParseError;
            this.datetimeParseError = datetimeParseError;
        }

		#endregion

		#region BindField.

		/// <summary>
		/// Binds a field to UI element.
		/// </summary>
		/// <param name="field">Business object's field.</param>
		/// <param name="ctl">Control to which the field is bound.</param>
        public IFieldBinding BindField(IBindableField field, IBoundControl ctl)
		{
            FieldBinding newBinding = new FieldBinding(field, ctl);
			this.bindings.Add(newBinding);
            return newBinding;
		}

        /// <summary>
        /// Binds a field to UI element.
        /// </summary>
        /// <param name="fieldBindingName">Name of the property that is displayed and modified in binding engine. Null is <b>not</b> allowed.</param>
        /// <param name="fieldDataType">Type of the value stored in the property. Null is <b>not</b> allowed.</param>
        /// <param name="ctl">Control to which the field is bound.</param>
        public IFieldBinding BindField(string fieldBindingName, Type fieldDataType, IBoundControl ctl)
        {
            IBindableField field = new BindableField(fieldBindingName, fieldDataType);
            FieldBinding newBinding = new FieldBinding(field, ctl);
            this.bindings.Add(newBinding);
            return newBinding;
        }

        /// <summary>
        /// Binds a field to UI element.
        /// </summary>
        /// <param name="fieldBindingName">Name of the property that is displayed and modified in binding engine. Null is <b>not</b> allowed.</param>
        /// <param name="fieldDataType">Type of the value stored in the property. Null is <b>not</b> allowed.</param>
        /// <param name="fieldCaptionProvider">Object that provides caption for the bound field. Nullable.</param>
        /// <param name="ctl">Control to which the field is bound.</param>
        /// <param name="satelliteFields">Optional satellite fields. See <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>.</param>
        public IFieldBinding BindField(string fieldBindingName, Type fieldDataType, ICaptionProvider fieldCaptionProvider, IBoundControl ctl, params string[] satelliteFields)
        {
            IBindableField field = new BindableField(fieldBindingName, fieldDataType, fieldCaptionProvider, satelliteFields);
            FieldBinding newBinding = new FieldBinding(field, ctl);
            this.bindings.Add(newBinding);
            return newBinding;
        }

        /// <summary>
        /// Binds a field to UI element.
        /// </summary>
        /// <param name="fieldBindingName">Name of the property that is displayed and modified in binding engine. Null is <b>not</b> allowed.</param>
        /// <param name="caption">Caption/description of the bound field. Nullable.</param>
        /// <param name="fieldDataType">Type of the value stored in the property. Null is <b>not</b> allowed.</param>        
        /// <param name="ctl">Control to which the field is bound.</param>
        /// <param name="satelliteFields">Optional satellite fields. See <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>.</param>
        public IFieldBinding BindField(string fieldBindingName, string caption, Type fieldDataType, IBoundControl ctl, params string[] satelliteFields)
        {
            IBindableField field = new BindableField(fieldBindingName, caption, fieldDataType, satelliteFields);
            FieldBinding newBinding = new FieldBinding(field, ctl);
            this.bindings.Add(newBinding);
            return newBinding;
        }

		#endregion

		#region SetMainErrorLabel.

		/// <summary>
        /// Sets main/default error label to which all unhandled errors are written.
		/// </summary>
		/// <param name="label">Error label.</param>
		/// <remarks>Errors that aren't handled by specialised error labels are emited to the main error label.</remarks>
        public void SetMainErrorLabel(ITextControl label)
		{
			this.mainErrorLabel = label;
		}

		#endregion

		#region PropagateAccessMode.

		/// <summary>Changes and propagates the specified access mode to all controls used in the current binding context.</summary>
		/// <param name="accessMode">New access mode.</param>
		public void PropagateAccessMode(FieldAccessMode accessMode)
		{
			foreach (FieldBinding binding in this.bindings)
			{
				if (binding.BoundControl != null)
					binding.BoundControl.AccessMode = accessMode;
			}
		}

		#endregion

		#region PopulateBusinessObject.

		/// <summary>
		/// Populates business object properties with the values stored in bound controls.
		/// If an errors is encountered, none of the bound properties will receive a new value.
		/// </summary>
		/// <param name="bizObject">Business object that is receiving the new values.</param>
		/// <param name="displayParseErrors">Indicates whether the method will automatically display 
		/// encountered parse errors. Parse error descriptions must be provided before PopulateBusinessObject 
		/// method is called.</param>
		/// <returns>Encountered errors. Each rule has an errorcode value to help identify the problem. 
		/// Errorcode is a string representation of ParseError enumeration values. These are UI specific
		/// errors. Eg. user entered "XYZ" in a control that is bound to an integer field.</returns>
		public BusinessRuleCollection PopulateBusinessObject(IValueObject bizObject, bool displayParseErrors)
		{
			BusinessRuleCollection errors = new BusinessRuleCollection();

			object[] values = new object[this.bindings.Count];
			for (int i=0; i<this.bindings.Count; i++)
			{
				FieldBinding currBinding = (FieldBinding) this.bindings[i];				
				
				// Skip read-only fields.
				if (currBinding.Field.IsReadOnly)
					continue;

				ParseError errorCode;
                string propertyName = currBinding.Field.BindingName;
                var twoWayCtlProperty = ControlPropertyBinderFactory.CreateBindableControlProperty(currBinding.BoundControl);
                object val = twoWayCtlProperty.GetValue(currBinding.BoundControl, currBinding.BoundControl.TwoWayBoundProperty, currBinding.Field.DataType, this.formatter, out errorCode);
				values[i] = val;
				if (errorCode != ParseError.AllOk)
				{
					// Client may analyse the error-code and may choose to display a more appropriate error description.
					BusinessRule error = new BusinessRule();
					error.AffectedFields = new string[]{propertyName};
					error.ErrorCode = errorCode.ToString();					
					error.IsBroken = true;
					error.Description = BuildParseErrorDescription(currBinding.Field.Caption, errorCode);
					errors.Add(error);
				}
			}

			if (errors.Count == 0)
			{
				// Bind if no parsing errors occurred.
				for (int i=0; i<this.bindings.Count; i++)
				{
					IFieldBinding currBinding = this.bindings[i];

					// Skip read-only fields.
					if (currBinding.Field.IsReadOnly)
						continue;

					string propertyName = currBinding.Field.BindingName;
					bizObject.SetPropertyValue(propertyName, values[i]);
				}
			}
			else if (displayParseErrors)		
			{
				// Parse errors occurred. Client wants them to be automatically displayed.				
				DisplayErrors(errors);
			}

			return errors;
		}

		private string BuildParseErrorDescription(string fieldCaption, ParseError errorCode)
		{
			// If dedicated error description is not available, return default description.

			StringBuilder error = new StringBuilder();
			string description;
			switch (errorCode)
			{
				case ParseError.WrongDateFormat:
					description = (this.datetimeParseError != null) ? this.datetimeParseError : this.defaultParseError;
					break;

				case ParseError.WrongIntegerFormat:
					description = (this.integerParseError != null) ? this.integerParseError : this.defaultParseError;
					break;

				case ParseError.WrongNumberFormat:
					description = (this.numberParseError != null) ? this.numberParseError : this.defaultParseError;
					break;

				default:
					description = this.defaultParseError;
					break;
			}

			if (description != null  &&  description.Length > 0)
			{
				if (fieldCaption != null  &&  fieldCaption.Length > 0)
				{
					error.Append(char.ToUpper(fieldCaption[0]));
					if (fieldCaption.Length > 1)
						error.Append(fieldCaption.Substring(1));
					error.Append(" - ");
					error.Append(char.ToLower(description[0]));
					if (description.Length > 1)
						error.Append(description.Substring(1));
					if (error[error.Length-1] != '.')
						error.Append('.');
				}
				else
				{
					error.Append(char.ToUpper(description[0]));
					if (description.Length > 1)
						error.Append(description.Substring(1));
					if (error[error.Length-1] != '.')
						error.Append('.');
				}
			}

			return error.ToString();
		}

		#endregion

		#region DisplayBusinessObject.

		/// <summary>Display data stored in business object. Controls are populated with values stored in the business object.</summary>
		/// <param name="bizObject">Business object whose values are copied to controls.</param>
		public void DisplayBusinessObject(IValueObject bizObject)
		{
			foreach (FieldBinding binding in this.bindings)
			{
				Type bizObjectType = bizObject.GetType();
				PropertyInfo bizProperty = bizObjectType.GetProperty(binding.Field.BindingName);
				if (bizProperty == null)
					throw new Exception("Business object doesn't contain the specified property: " + binding.Field.BindingName + ".");
				object val = bizProperty.GetValue(bizObject, null);
                var twoWayProperty = ControlPropertyBinderFactory.CreateBindableControlProperty(binding.BoundControl);
                twoWayProperty.SetValue(binding.BoundControl, binding.BoundControl.TwoWayBoundProperty, val, this.formatter);

				// Satellite fields. Number of satellite fields in business object and control may differ.
				int satelliteFieldsCount = 0;
				if (binding.Field.SatelliteFields != null  &&  binding.BoundControl.OneWayBoundProperties != null)
				{
                    if (binding.Field.SatelliteFields.Length < binding.BoundControl.OneWayBoundProperties.Length)
						satelliteFieldsCount = binding.Field.SatelliteFields.Length;
					else
                        satelliteFieldsCount = binding.BoundControl.OneWayBoundProperties.Length;
				}

				for (int i=0; i<satelliteFieldsCount; i++)
				{
					object satelliteReadOnlyValue = DataBinder.Eval(bizObject, binding.Field.SatelliteFields[i]);
                    var oneWayProperty = ControlPropertyBinderFactory.CreateBindableControlProperty(binding.BoundControl, binding.BoundControl.OneWayBoundProperties[i]);
                    oneWayProperty.SetValue(binding.BoundControl, binding.BoundControl.OneWayBoundProperties[i], satelliteReadOnlyValue, this.formatter);
				}

				if (bizObject.IsReadOnly  &&  binding.BoundControl != null)
					binding.BoundControl.AccessMode = FieldAccessMode.ReadOnly;
			}
		}

		#endregion

		#region ClearAllErrors.

		/// <summary>Clears errors in all bound controls.</summary>
		public void ClearAllErrors()
		{
			foreach (FieldBinding binding in this.bindings)
			{
				if (binding.BoundControl != null)
				{
                    //binding.BoundControl.ClearError();
                    if (binding.BoundControl.ErrorLabel != null)
                        binding.BoundControl.ErrorLabel.Text = null;
				}
				else if (binding.ErrorLabel != null)
				{   
					binding.ErrorLabel.Text = "";
                    Control ctlErrorLable = (binding.ErrorLabel as Control);
                    if (ctlErrorLable != null)
                        ctlErrorLable.Visible = false;
				}
			}
                     
			this.mainErrorLabel.Text = "";
            Control ctlMainErrLbl = this.mainErrorLabel as Control;
            if (ctlMainErrLbl != null)
                ctlMainErrLbl.Visible = false;
		}

		#endregion

		#region DisplayErrors.

		/// <summary>Displays all rules in the given collection. Typically used to display errors.</summary>
		/// <param name="brokenRules">Collection of broken rules.</param>
		public void DisplayErrors(BusinessRuleCollection brokenRules)
		{
			// Per field errors.
			StringCollection handledFields = new StringCollection();
			foreach (IFieldBinding binding in this.bindings)
			{
				handledFields.Add(binding.Field.BindingName);
				if (binding.BoundControl != null)
				{
                    //binding.BoundControl.AppendError(brokenRules, this.formatter);
                    BusinessRuleCollection brokenRulesForThisControl = brokenRules.GetRulesForField(binding.Field.BindingName, true);
                    string errorText = brokenRulesForThisControl.ToString(formatter) + " ";
                    if (binding.BoundControl.ErrorLabel != null)
                        binding.BoundControl.ErrorLabel.Text += errorText;
                    else if (this.mainErrorLabel != null)
                        this.mainErrorLabel.Text += errorText;
				}
				else if (binding.ErrorLabel != null)
				{	
					BusinessRuleCollection brokenRulesForThisControl = brokenRules.GetRulesForField(binding.Field.BindingName, true);
					string errmsg = brokenRulesForThisControl.ToString(this.formatter);
					
					if (errmsg != null  &&  errmsg.Trim() != "")
					{
						binding.ErrorLabel.Text += errmsg + " ";
                        Control ctlErrorLable = (binding.ErrorLabel as Control);
                        if (ctlErrorLable != null)
                            ctlErrorLable.Visible = true;
					}			
				}
			}

			// Errors not associated with fields.
			if (this.mainErrorLabel != null)
			{
                foreach (string externallyHandledProperty in this.externallyValidatedProperties)
                    handledFields.Add(externallyHandledProperty);

				string[] arrHandledFields = new string[handledFields.Count];
				handledFields.CopyTo(arrHandledFields, 0);
				BusinessRuleCollection unhandledErrors = brokenRules.GetUnhandledRules(arrHandledFields, true);
				if (unhandledErrors.Count > 0)
				{
					this.mainErrorLabel.Text = unhandledErrors.ToString(this.formatter);
                    Control ctlMainErrLbl = this.mainErrorLabel as Control;
                    if (ctlMainErrLbl != null)
                        ctlMainErrLbl.Visible = true;
				}
			}			
		}

		#endregion

		#region Properties - parse error messages.

		/// <summary>
		/// Gets or sets the generic message displayed when a parsing error occures.
		/// </summary>
		public string DefaultParseError
		{
			get {return this.defaultParseError;}
			set {this.defaultParseError = value;}
		}

		/// <summary>
		/// Gets or sets the generic message displayed when parsing of integer value fails.
		/// </summary>
		public string IntegerParseError
		{
			get {return this.integerParseError;}
			set {this.integerParseError = value;}
		}

		/// <summary>
		/// Gets or sets the generic message displayed when number parsing fails.
		/// </summary>
		public string NumberParseError
		{
			get {return this.numberParseError;}
			set {this.numberParseError = value;}
		}

		/// <summary>
		/// Gets or sets the generic message displayed when parsing of DateTime value fails.
		/// </summary>
		public string DatetimeParseError
		{
			get {return this.datetimeParseError;}
			set {this.datetimeParseError = value;}
		}

		#endregion

        #region ExternallyValidatedProperties.

        StringCollection externallyValidatedProperties = new StringCollection();

        /// <summary>
        /// Gets a collection which defines properties whose errors are handled and displayed externally, ie. will not be displayed in main error label.
        /// </summary>
        /// <value>A collection of property names.</value>
        /// <remarks>Typically used to suppress child collection errors when they are handled and displayed by other components such as grids.</remarks>
        public StringCollection ExternallyValidatedProperties
        {
            get { return this.externallyValidatedProperties; }
        }

        #endregion

        /// <summary>Gets field bindings.</summary>
        /// <value>A collection of <see cref="IFieldBinding"/> objects.</value>
        public IEnumerable<IFieldBinding> FieldBindings
        {
            get { return this.bindings; }
        }
    }
}
